tree grammar JKlasyTypeChecker;

options {
	tokenVocab=JKlasy;
	ASTLabelType=Tree;
}

@header {
package compiler.typechecker;

import compiler.common.*;
}

@members {
	Environment env;
	
    public Environment getEnvironment() {
        return this.env;
    }
    public void setEnvironment(Environment env) {
    	this.env = env;
    }
}

program returns[Environment e]
	: ^(PROGRAM
	  main_part { env.resetVars(); }
		  ( class_definition 
		  	{ 
		  		// reset variables map after main part and each class def
		  		env.resetVars(); 
		  	} 
		  )*
		)
		{ e = this.env; }
	;

main_part
	: ^(MAINPART variable_declaration* statements)
	;
	
class_definition
	: ^(CLASSDEFINITION ID 
		{ env.setInsideType($ID.text); /* we are inside class def */}
		(m=operation_descriptor)* 
	  		fields_declaration
	  		( 
	  		  {/* save variables visible now, and restore it after algorithms */}
	  			{this.env.saveVars(); } 
	  			operation_algorithm  
	  			{this.env.restoreVars(); }
	  		)*
	  	)
	  	{
	  		try {
	  			// checks if every declared method is implemented
	  			env.checkImplementations($ID.text);
	  		}
	  		catch(Exception e) {
	  			env.addError(e.getMessage());
	  		}
	  	}
	;
	
fields_declaration:
  variable_declaration*
  ;	
	
variable_declaration
	: ^(VARIABLEDECLARATION 
		(d=declaration_type ID 
		  {
		  	// checks if type d is defined
		  	if(!this.env.isDefined(d)) {
		  		env.addError("Unknown type '" + d + "'");
		  	}
		  	else {
			  	try { 
			  		// add variable and its type to environment 
			  		// (error if variable was added earlier 
					env.addVariable($ID.text, d);
			  	} 
			  	catch(Exception e) {
			  		this.env.addError(e.getMessage());
			  	} 
		  	}
		  } 
		)+
	   )
	;

declaration_type returns[String typename]
	: ^(DECLARATIONTYPE s=simple_type
	  {
	  	typename = s;
	  } 
	 (^(ARRAY OF INTNUMBER) { typename = "array of " + typename; } )?)
	;

parameter_type returns[String typename]
	: ^(PARAMETERTYPE 
		s=simple_type
		{
		  	typename = s;
		} 
		(^(ARRAY OF)  { typename = "array of " + typename; }  )?
	  ) 
	;
	
simple_type returns[String type]
	: INTT { type = "integer"; }
	| BOOLT { type = "boolean"; }	  
	| STRINGT { type = "string"; }
	| REALT   { type = "real"; }
	| CHART	 { type = "char"; } 
	| ID { type = $ID.text; }     
	;
	
return_type returns[String type]
	: ^(RESULT pt=parameter_type? {type = pt;} )
	;

modifier
	: PRIVATE
	| PUBLIC
	;
	
operation_descriptor
	: ^(OPERATIONDESCRIPTOR modifier?  
		d=designator  
		(p=parameter_type
		 {
		 	// checks if every type in method arguments is defined
		 	if(!env.isDefined(p))
		 		env.addError("Unknown type '" + p + "' in '" + d + "' method declaration");
		 }
		)* 		
    	rt=return_type
		 {
		 	// checks the same for return type
		 	if(rt != null && !env.isDefined(rt))
		 		env.addError("Unknown type '" + p + "' in '" + d + "' method declaration");
		 }
	  )
	;
	
designator returns[String name]
	: ID {name=$ID.text;}
	| ro=relational_operator {name=$ro.name;}
	| ao=additive_operator {name=$ao.name;}
	| mo=multiplicative_operator {name=$mo.name;}
	;

operation_algorithm
	: ^(OPERATIONALGORITHM methodname=designator 
		^(ARGUMENTS 
			{ 
				List<String> ids =  new ArrayList<String>();  // list of variables names
				List<String> types = new ArrayList<String>(); // list of variable types
				TypeTC t1 = this.env.findType(this.env.getInsideType()); // get type we are in
				boolean hasreturn = false;					  // assume we have no return from method
			}
			( (ID {ids.add($ID.text); /* add new var names */ } )+ )?
			{ 				
				try {
					// get argument type list for this method
					types = t1.getMethodArgTypes(methodname, ids.size());
					// add variables and its type to environment
					for(int i = 0; i < ids.size(); i++)
						env.addVariable(ids.get(i), types.get(i));
				}
			  	catch(Exception e) {
			  		this.env.addError(e.getMessage());
			  	} 
			}
		) 
	  	^(VARIABLES variable_declaration*) 
	  	statements 
	  	(^(RETURN t=expression {hasreturn = true; /* we have return from method */ } ))?)
	  	{
	  		try {
	  			String rt = null;
	  			// sets special method return type to #void if there is no return
	  			if(!hasreturn)
	  				t = "#void";
	  			// get method return type from our type
	  			rt = t1.getMethodReturnType(methodname, types, false);
	  			// check types equality
		  		if((t!= null && rt != null && t.equals(rt)) || (t == null && rt == null))
		  			t1.setImplementation(methodname, types, t);
		  		else
		  			env.addError("Bad return type in method '" + methodname + "' or no return point from method");
	  		}
	  		catch (Exception e) {
	  			env.addError(e.getMessage());
	  		}
	  	}
	;

statements
	: ^(STATEMENTS statement*)
	;

statement
    : assign_statement
	| if_statement
	| while_statement
	| input_statement
	| output_statement
	| call_statement
	;

if_statement
	: ^(IFSTATEMENT type=expression 
	   conditional_option 
	   conditional_option?)
	   {
	   	 if(!type.equals("boolean"))
	   	   env.addError("Expression in if statement should be boolean" + 
	   	   " type instead of " + type);
	   }
	;
	
conditional_option
	: statement
	| statements
	;
	
assign_statement
	: ^(ASSIGNSTATEMENT
	    { boolean ind=false; } 
		ids=extended_id (INDEX itype=expression {ind=true;} )? type=expression
	    {	 
	    	String type1 = null;
	    	boolean err = false;	
		  	try {
		  		type1=env.checkProperField(ids, env.getVariableTypeName(ids.get(0)) );
		  	}
		  	catch (Exception e) {
		  		env.addError(e.getMessage());
		  		err = true;
		  	}
		  	if(!err) {
		  		if(ind) {
		  			if(!itype.equals("integer")) {
		  				env.addError("Expression used as a table index should have integer type");
		  				err = true;
		  			}
		  			else if(type1.startsWith("array of "))
				  		type1=type1.substring(9);
				    else {
				    	env.addError("Variable is not an array");
				    	err = true;
				    }
		  		}
		  		if(!err) {
				  	boolean cond = false;
				  	if(type.equals("#null") && !type1.equals("integer") &&  //przypisanie nulla do typu obiektowego
				  		!type1.equals("real") && !type1.equals("string") &&
				  		!type1.equals("boolean") && !type1.equals("char") &&
				  		!type1.equals("array of integer") &&  !type1.equals("array of real") && 
				  		!type1.equals("array of string") && !type1.equals("array of boolean") && 
				  		!type1.equals("array of char"))
				  		cond = true;
				  	if(!type1.equals(type) && !type.equals("#error") && !cond)
				  	    env.addError("Trying to assign expression of type '" + 
				  	    type + "' to variable of type '" + type1 + "'");
		  		}
		  	}
	    }
	  )  	
	;
	
call_statement returns[String type]
	: ^(CALLSTATEMENT t=call {type = t;}  )
	;

call returns[String type]
	@init { List<String> types = new ArrayList<String>(); }
	: ^(CALL ids=extended_id (t=expression  { types.add(t); } )*
	   {
   		String type1 = null;
   		int s = ids.size();
   		String methodid = ids.get(s - 1); 
        ids = ids.subList(0, s - 1);
	  	try {
	  		if(ids.size() == 0 && env.hasGlobalFun(methodid, types)) {
	  			type = env.getGlobalFunRet(methodid, types);
	  		}
	  		else if(ids.size() > 0)
	  		{
	  			type1=env.checkProperField(ids, env.getVariableTypeName(ids.get(0)) );
		  		TypeTC ts = env.findType(type1);
		  		type = ts.getMethodReturnType(methodid, types, true);
	  		}
	  		else
	  		{
	  			if(env.isDefined(methodid)) {
	  				TypeTC ts = env.findType(methodid);
	  				if(ts.hasMethod(methodid, types))
	  					type = methodid;
	  				else {
	  					env.addError("Undefined constructor for '" + methodid + "' type");
	  					type = "#error";
	  				}
	  			}
	  			else if(env.getInsideType() != null) {
	  				type1 = env.getInsideType();
	  				TypeTC ts = env.findType(type1);
	  				if(ts.hasMethod(methodid, types))
	  					type = ts.getMethodReturnType(methodid, types, false);
	  				else
	  				{
	  					env.addError("Undefined method '" + methodid + "' in '" + type + "'");
	  					type = "#error";
	  				}
	  			}
	  			else {
	  				env.addError("Undefined method '" + methodid + "' in main routine");
	  				type = "#error";
	  			}
	  		}	  			
	  	}
	  	catch (Exception e) {
	  		env.addError(e.getMessage());
	  		type = "#error";
	  	}
	   }
	  )
	;
	
while_statement
	: ^(WHILESTATEMENT type=expression statements) 
	   {
	   	 if(!type.equals("boolean"))
	   	   env.addError("Expression in if statement should be boolean" + 
	   	   " type instead of " + type);
	   }
	;

extended_id returns[List<String> ids]
	: ^(EXTENDEDID  { ids = new ArrayList<String>(); } 
	    (ID { ids.add($ID.text); } )+
	   )
	;
	  
input_statement
	: ^(INPUTSTATEMENT 
		(ids=extended_id
		{
		   try {
			   String type=env.checkProperField(ids, env.getVariableTypeName(ids.get(0)) );
			   if(! (type.equals("integer") || type.equals("real") || 
			   		type.equals("string")))
			   		env.addError("Input statement can be only used to types: " +
			   		"integer, real, string");
		   }
		   catch (Exception e) {
		   	  env.addError(e.getMessage());
		   }
		}
		)+ 
	  )	
	;
			
output_statement
	: ^(OUTPUTSTATEMENT 
		(
			t=expression
			{
				if(!(t.equals("integer") || t.equals("real") ||
				   t.equals("char") || t.equals("string") ||
				   t.equals("boolean")))
				   env.addError("Unexpected type in output statement");
			}
		)+ 
	  )
	;
	
expression returns[String type]
	: ^(EXPRESSION t=expr {type = t;} )
	;

expr returns[String type]
	@init{ 
		List<String> argtypes = null;
		}
	: ^(EQUAL type1=expr type2=expr) 
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
		if (type1.equals(type2) && env.hasMethod(type1, "=", argtypes))
	      type = "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type = "#error";
	    else {
	      this.env.addError("Trying to compare values of types: " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(NOTEQ type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
		if (type1.equals(type2) && env.hasMethod(type1, "!=", argtypes ))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else {
	      this.env.addError("Trying to compare values of types: " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(GR type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
		if (type1.equals(type2) && env.hasMethod(type1, ">", argtypes))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Trying to compare values of types: " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(GREQ type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
		if (type1.equals(type2) && env.hasMethod(type1, ">=", argtypes))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Trying to compare values of types: " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(LEQ type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
		if (type1.equals(type2) && env.hasMethod(type1, "<=", argtypes))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Trying to compare values of types: " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(LS type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
		if (type1.equals(type2) && env.hasMethod(type1, "<", argtypes))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Trying to compare values of types: " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(AND type1 = expr type2 = expr)
	  {
		if (type1.equals(type2) && type1.equals("boolean"))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types: expected booleans, got " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(OR type1 = expr type2 = expr)
	  {
		if (type1.equals(type2) && type1.equals("boolean"))
	      type =  "boolean";
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types: expected booleans, got " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(PLUS type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
	  	if (type1.equals(type2) && env.hasMethod(type1, "+", argtypes))
	  	  type =  type1;
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types for '+': " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(MINUS type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
	  	if (type1.equals(type2) && env.hasMethod(type1, "-", argtypes))
	  	  type =  type1;
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types for '-': " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(DIV type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
	  	if (type1.equals(type2) && env.hasMethod(type1, "div", argtypes))
	  	  type =  type1;
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types for 'div': " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(MOD type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
	  	if (type1.equals(type2) && env.hasMethod(type1, "mod", argtypes))
	  	  type =  type1;
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types for 'mod': " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| ^(MULT type1 = expr type2 = expr)
	  {
	  	argtypes = new ArrayList<String>();
	    argtypes.add(type1);
	    argtypes.add(type2);
	  	if (type1.equals(type2) && env.hasMethod(type1, "*", argtypes))
	  	  type =  type1;
	    else if(type1.equals("#error") || type2.equals("#error"))
	   	  type =  "#error";
	    else
	    {
	      this.env.addError("Invalid argument types for '*': " +
	        	type1 + " and " + type2);
	   	  type =  "#error";
	    }
	  }
	| INTNUMBER 
	  {
		type =  "integer";
	  }
	| REALNUMBER
	  {
	    type =  "real";
	  }
	| TRUE
      {
        type =  "boolean";
      }
	| FALSE
	  {
	    type =  "boolean";
	  }
	| STRING
	  {
	    type =  "string";
	  }
	| CHAR
	  {
	    type =  "char";
	  }
	| NULL
	  {
	    type =  "#null";
	  }
	| ^(VAREXPR ids=extended_id 
	  {
	  	try {
	  		type=env.checkProperField(ids, env.getVariableTypeName(ids.get(0)) );
	  	}
	  	catch (Exception e) {
	  		env.addError(e.getMessage());
	  		type="#error";
	  	}
	  }	
	  (^(INDEX type1=expression)
	  {
	  	if(!type1.equals("integer")) {
	  		env.addError("Expression used as a table index should have integer type");
	  		type = "#error";	  		
	  	}
	  	else if(type.startsWith("array of "))
	  		type=type.substring(9);
	  	else {
	  		env.addError("Variable is not an array");
	  		type = "#error";
	  	}
	  }
	  )? 
	  )
	| ^(NOT type1 = expr) 
		{
		  if (type1.equals("boolean"))
		    type =  type1;
		  else
		  {
		    this.env.addError("Trying to negate value of non-boolean type");
		    type = "#error";
		  }
		}
	| ^(CAST type1 = expr type2 = simple_type)
		{
		  if(type1.equals("integer") && 
		  	  (type2.equals("real") || type2.equals("string") ||
		  	  type2.equals("boolean") || type2.equals("integer"))
		  	)	
		  	type =  type2;
		  else if(type1.equals("real") && 
		  	  (type2.equals("integer") || type2.equals("string") ||
		  	  type2.equals("real"))
		  	)	
		  	type =  type2;
		  else if(type1.equals("char") && 
		  	  (type2.equals("string") || type2.equals("char"))
		    )	
		  	type =  type2;
		  else if(type1.equals("boolean") && 
		  	  (type2.equals("integer") || type2.equals("real") ||
		  	  type2.equals("boolean"))
		  	)	
		  	type =  type2;
		  else if(type1.equals("string") && type2.equals(type1))
		    type = type2;
		  else {
		  	env.addError("Cannot cast type '" + type1 + "' to type '" + type2 + "'");
		  	type = "#error";
		  }		  
		}
	| type1 = call
		{
		  type =  type1;
		}
	;
	
boolean_operator
	: AND
	| OR
	;
	
relational_operator returns[String name]
	: EQUAL {name="=";}
	| NOTEQ {name="!=";}
	| GR  {name=">";}
	| GREQ {name=">=";}
	| LS {name="<";} 
	| LEQ {name="<=";}
	;

additive_operator returns[String name]
	: PLUS {name="+";}
	| MINUS {name="-";}
	;

multiplicative_operator returns[String name]
	: DIV  {name="div";}
	| MOD {name="mod";}
	| MULT {name="*";}
	;	

operator
	: relational_operator
	| additive_operator
	| multiplicative_operator
	| boolean_operator
	;